Skip to content

feat: /caveman <level> persists across sessions (symmetric)#450

Open
dead-developers wants to merge 2 commits into
JuliusBrussee:mainfrom
dead-developers:feat/persist-off-mode
Open

feat: /caveman <level> persists across sessions (symmetric)#450
dead-developers wants to merge 2 commits into
JuliusBrussee:mainfrom
dead-developers:feat/persist-off-mode

Conversation

@dead-developers

@dead-developers dead-developers commented May 26, 2026

Copy link
Copy Markdown

What's broken today

The slash command /caveman only switches caveman for the current session. The SessionStart hook re-activates whatever the SKILL/hook default is on every session resume — even after the user explicitly asked for a different level (most painfully, off).

Verbatim user reactions from real sessions:

  • "i thought i turned caveman off?"
  • "but the caveman skill itself says you can turn it off and that should persist??"

SKILL.md said "Level persist until changed or session end" — which is a documentation of the limitation, not a desirable property. Persistent levels via ~/.config/caveman/config.json already work (getDefaultMode() reads it, caveman-activate.js honors defaultMode: "off" and would honor any other valid level the user wrote there). But the user-facing slash command never wrote that file, so anyone who didn't know about the config layer just saw the bug.

What this PR changes

/caveman <level> now writes defaultMode: "<level>" to the platform's caveman config file. The choice persists across sessions in both directions/caveman off saves off, /caveman full saves full, etc. The persistent choice is whatever the user last asked for. Clearing it is a single action: delete the config file.

commands/caveman.toml — replaced the one-liner prompt with a "resolve → persist → act" flow:

  1. Resolve the requested level (with aliases: stop/disable/persist-offoff, on/enablefull, wenyanwenyan-full). Reject unknown levels loudly.
  2. Merge {"defaultMode": "<level>"} into ~/.config/caveman/config.json (or %APPDATA%\caveman\config.json on Windows, or $XDG_CONFIG_HOME/caveman/...). Existing JSON fields preserved.
  3. Apply the level to the current session.

skills/caveman/SKILL.md — Persistence and Boundaries sections rewritten to describe symmetric persistence and clarify that the slash command itself is the persistence mechanism (no separate /caveman persist and no hand-editing JSON required).

Why symmetric

The first revision of this PR (90cd5ac) only persisted off — other levels stayed session-scope. That produced the next confusing UX: after /caveman off, running /caveman full re-enabled caveman for the current session, but the config still said off, so the next session resume was silent again. Users had to know to delete the config file by hand.

The current revision (66f98a8) makes it symmetric. The mental model becomes: "/caveman <level> sets your caveman level. It persists. Delete the config to reset."

Compatibility

The hook layer already handles every defaultMode value this command writes — no caveman-config.js or caveman-activate.js changes. Users who never type /caveman see no behavior change.

Users who were relying on slash commands being session-scope (probably few — there's no obvious upside) can still set CAVEMAN_DEFAULT_MODE in their shell env, which overrides the config file per the existing precedence in getDefaultMode().

Test plan

  • /caveman off in a session: bot drops caveman style; config file contains {"defaultMode": "off"}.
  • Start a new session: SessionStart hook outputs OK only, no "CAVEMAN MODE ACTIVE" reminder.
  • /caveman full in that session: caveman resumes; config now {"defaultMode": "full"}.
  • Start a new session: hook outputs the full ruleset reminder.
  • /caveman ultra: caveman switches to ultra; config now {"defaultMode": "ultra"}. Next session is ultra.
  • /caveman nonsense: command refuses with the valid level list, no file write.
  • Pre-existing fields in ~/.config/caveman/config.json (e.g. user-added theme: ...) are preserved after every write.

The slash command previously only switched intensity for the current
session. Saying "stop caveman" or running `/caveman off` would silence
the bot mid-conversation but the SessionStart hook would re-activate
caveman on the next session resume — even after the user explicitly
opted out.

This surprised users (verified against real chats: "i thought i turned
caveman off?" / "the caveman skill itself says you can turn it off and
that should persist??"). SKILL.md said level persists "until changed or
session end", but that's a documentation of the limitation, not a
desirable behavior for the off case.

The plumbing for persistent off already exists upstream:
- `src/hooks/caveman-config.js::getDefaultMode()` reads
  `~/.config/caveman/config.json` (or the platform equivalent) and
  honors a `defaultMode` field.
- `src/hooks/caveman-activate.js` skips activation entirely when
  `defaultMode === "off"`.

What was missing: a path from the user typing `/caveman off` to that
config file getting written. The slash command's prompt didn't mention
the config layer; users had to know about it and edit the JSON by hand.

This commit closes that gap:

1. `commands/caveman.toml` — the prompt now special-cases `off` (and
   accepts the synonyms `stop`, `disable`, `persist-off`). For those
   args the model writes `{"defaultMode": "off"}` to the platform's
   caveman config file, merging into any existing JSON rather than
   clobbering. All other args (lite/full/ultra/wenyan-*) keep the
   existing session-only behavior.

2. `skills/caveman/SKILL.md` — the Persistence and Boundaries sections
   now make the session-vs-persistent distinction explicit and document
   the new `/caveman off` behavior plus how to re-enable.

The change is additive — any user who never types `off` sees no
behavior change.

Re-enable path: `/caveman full` (or another non-off level) puts the
session back in caveman mode immediately. To clear the persistent-off
config, the user can edit `~/.config/caveman/config.json` and remove
the `defaultMode` field, or delete the file entirely.
The first revision of this PR only persisted `/caveman off`. Other
levels (full/lite/ultra/wenyan-*) stayed session-scope. That's
asymmetric and surprising: after a persistent off, running
`/caveman full` would re-enable caveman for the current session but
leave `defaultMode: "off"` in the config, so the next session resume
was silent again. Users had to know to delete the config file.

Symmetric semantics are cleaner: any `/caveman <level>` writes
`defaultMode: "<level>"` to the config file. `off` saves off; `full`
saves full; etc. The persistent choice is whatever the user last
asked for. Falling back to the built-in default of `full` is a
single action: delete the config file.

Changes:

- `commands/caveman.toml`: rewrote the prompt around a single "resolve
  + persist + act" flow. Accepts aliases (stop/disable/persist-off → off,
  on/enable → full, wenyan → wenyan-full) and rejects unknown levels
  loudly with the valid set. Writes the resolved level to the config
  every time, merging into any existing JSON.

- `skills/caveman/SKILL.md`: updated Persistence + Boundaries sections
  to describe symmetric persistence and call out that the slash command
  itself is the persistence mechanism — no separate "/caveman persist"
  or hand-editing required.

The hook layer (`src/hooks/caveman-config.js`, `caveman-activate.js`)
already handles every `defaultMode` value this command might write —
no hook changes needed.
@dead-developers dead-developers changed the title feat: /caveman off persists across sessions feat: /caveman <level> persists across sessions (symmetric) May 27, 2026
dead-developers added a commit to dead-developers/vibespeak that referenced this pull request May 27, 2026
Caveman shipped a persistence fix (JuliusBrussee/caveman#450) that
lets `/caveman <level>` write a config file the SessionStart hook
honors on every resume — symmetric in both directions, including off.

Vibespeak has the structurally identical bug today: `/vibespeak off`
is session-only, the SessionStart hook re-fires the announcement on
every reload, and SKILL.md claims "off" persists when it doesn't.

This file is a self-contained porting guide for the next Claude Code
session that opens this repo. It maps each caveman PR change to its
vibespeak equivalent, lists the files to add and modify, and provides
the test plan + suggested commit message. Once the work lands, delete
the file — README + SKILL.md are the durable docs.
dead-developers pushed a commit to dead-developers/vibespeak that referenced this pull request May 27, 2026
Mirrors the persistence fix shipped for the sibling caveman plugin
(JuliusBrussee/caveman#450). /vibespeak off and the level switches
now write {"defaultMode": "<level>"} to ~/.config/vibespeak/config.json
(or the platform equivalent). The SessionStart hook reads this and
either skips activation (off) or applies the saved level on every
resume.

Same shape as the caveman PR: add hooks/vibespeak-config.js with
getDefaultMode() + safeWriteFlag(), gate vibespeak-activate.js on the
saved mode, teach commands/vibespeak{,-off,-short,-chatty}.md to write
the config when invoked.

Symmetric in both directions - /vibespeak off saves off,
/vibespeak normal saves normal. Clearing the persistent choice is a
single action: delete the config file.

Test plan executed before commit:
1. No config: hook emits VIBESPEAK MODE ACTIVE reminder (default).
2. {"defaultMode":"off"} written: hook outputs "OK" only, no reminder.
3. Config deleted: reverts to default behavior.
4. VIBESPEAK_DEFAULT_MODE=off env var: honored over config file.
5. Pre-existing fields in config: preserved (hook only reads defaultMode).
6. Invalid defaultMode value: falls through to built-in "normal" default.

See HANDOFF-persistence.md for the full porting notes (deleted in a
follow-up commit once the work landed).
@dead-developers

Copy link
Copy Markdown
Author

fixes persistence. turn it on, and it stays on. turn it off and it stays off. across all sessions.

currently using.

hope it helps anyone ¯_(ツ)_/¯

dead-developers added a commit to dead-developers/vibespeak that referenced this pull request Jun 7, 2026
Mirrors the persistence fix shipped for the sibling caveman plugin
(JuliusBrussee/caveman#450). /vibespeak off and the level switches
now write {"defaultMode": "<level>"} to ~/.config/vibespeak/config.json
(or the platform equivalent). The SessionStart hook reads this and
either skips activation (off) or applies the saved level on every
resume.

Same shape as the caveman PR: add hooks/vibespeak-config.js with
getDefaultMode() + safeWriteFlag(), gate vibespeak-activate.js on the
saved mode, teach commands/vibespeak{,-off,-short,-chatty}.md to write
the config when invoked.

Symmetric in both directions - /vibespeak off saves off,
/vibespeak normal saves normal. Clearing the persistent choice is a
single action: delete the config file.

Test plan executed before commit:
1. No config: hook emits VIBESPEAK MODE ACTIVE reminder (default).
2. {"defaultMode":"off"} written: hook outputs "OK" only, no reminder.
3. Config deleted: reverts to default behavior.
4. VIBESPEAK_DEFAULT_MODE=off env var: honored over config file.
5. Pre-existing fields in config: preserved (hook only reads defaultMode).
6. Invalid defaultMode value: falls through to built-in "normal" default.

See HANDOFF-persistence.md for the full porting notes (deleted in a
follow-up commit once the work landed).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant